#include "catch2/catch.hpp"

#include "translator.hpp"
#include "translation_provider_CPUFLASH.hpp"
#include <iostream>
#include <fstream>
#include <stdio.h>
#include <deque>
#include <map>
#include <set>
#include "hash.hpp"
#include "fnt-indices.hpp"
#include "provider.h"

#define CHECK_MESSAGE(cond, msg) \
    do {                         \
        INFO(msg);               \
        CHECK(cond);             \
    } while ((void)0, 0)
#define REQUIRE_MESSAGE(cond, msg) \
    do {                           \
        INFO(msg);                 \
        REQUIRE(cond);             \
    } while ((void)0, 0)

using namespace std;

using TPBSH = CPUFLASHTranslationProviderBase::SHashTable;
/// This is needed for binary comparison of hash table generated by the build script
/// and the hash table built during these tests
const TPBSH::BucketRange hash_table_ForComparison[TPBSH::Buckets()] =
#include "hash_table_buckets.ipp"

    const TPBSH::BucketItem stringRecArrayForComparison[TPBSH::MaxStrings()] =
#include "hash_table_string_indices.ipp"

        constexpr size_t maxStringBegins = TPBSH::MaxStrings();
constexpr size_t maxUtf8Raw = 200000;

/// just like the StringTableCS, but without const data - to be able to fill them during testing at runtime
struct StringTableCSTest {
    // this will get statically precomputed for each translation language separately
    static uint16_t stringCount, stringBytes;
    static uint16_t stringBeginsSize;
    static uint32_t utf8RawSize, stringBegins[maxStringBegins];
    // a piece of memory where the null-terminated strings are situated
    static uint8_t utf8Raw[maxUtf8Raw];

    static void Reset() {
        stringBeginsSize = 0;
        utf8RawSize = 0;
        fill(stringBegins, stringBegins + maxStringBegins, 0);
        fill(utf8Raw, utf8Raw + maxUtf8Raw, 0);
    }
};

uint32_t StringTableCSTest::stringBegins[maxStringBegins], StringTableCSTest::utf8RawSize;
uint8_t StringTableCSTest::utf8Raw[maxUtf8Raw];
uint16_t StringTableCSTest::stringCount, StringTableCSTest::stringBytes;
uint16_t StringTableCSTest::stringBeginsSize;

using CPUFLASHTranslationProviderCSTest = CPUFLASHTranslationProvider<StringTableCSTest>;

struct StringTableDETest {
    // this will get statically precomputed for each translation language separately
    static uint16_t stringCount, stringBytes;
    static uint32_t utf8RawSize, stringBegins[maxStringBegins];
    static uint16_t stringBeginsSize;
    // a piece of memory where the null-terminated strings are situated
    static uint8_t utf8Raw[maxUtf8Raw];

    static void Reset() {
        stringBeginsSize = 0;
        utf8RawSize = 0;
        fill(stringBegins, stringBegins + maxStringBegins, 0);
        fill(utf8Raw, utf8Raw + maxUtf8Raw, 0);
    }
};

uint32_t StringTableDETest::stringBegins[maxStringBegins], StringTableDETest::utf8RawSize;
uint8_t StringTableDETest::utf8Raw[maxUtf8Raw];
uint16_t StringTableDETest::stringCount, StringTableDETest::stringBytes;
uint16_t StringTableDETest::stringBeginsSize;

using CPUFLASHTranslationProviderDETest = CPUFLASHTranslationProvider<StringTableDETest>;

struct StringTableESTest {
    // this will get statically precomputed for each translation language separately
    static uint16_t stringCount, stringBytes;
    static uint16_t stringBeginsSize;
    static uint32_t utf8RawSize, stringBegins[maxStringBegins];
    // a piece of memory where the null-terminated strings are situated
    static uint8_t utf8Raw[maxUtf8Raw];

    static void Reset() {
        stringBeginsSize = 0;
        utf8RawSize = 0;
        fill(stringBegins, stringBegins + maxStringBegins, 0);
        fill(utf8Raw, utf8Raw + maxUtf8Raw, 0);
    }
};

uint32_t StringTableESTest::stringBegins[maxStringBegins], StringTableESTest::utf8RawSize;
uint8_t StringTableESTest::utf8Raw[maxUtf8Raw];
uint16_t StringTableESTest::stringCount, StringTableESTest::stringBytes;
uint16_t StringTableESTest::stringBeginsSize;

using CPUFLASHTranslationProviderESTest = CPUFLASHTranslationProvider<StringTableESTest>;

struct StringTableFRTest {
    // this will get statically precomputed for each translation language separately
    static uint16_t stringCount, stringBytes;
    static uint16_t stringBeginsSize;
    static uint32_t utf8RawSize, stringBegins[maxStringBegins];
    // a piece of memory where the null-terminated strings are situated
    static uint8_t utf8Raw[maxUtf8Raw];

    static void Reset() {
        stringBeginsSize = 0;
        utf8RawSize = 0;
        fill(stringBegins, stringBegins + maxStringBegins, 0);
        fill(utf8Raw, utf8Raw + maxUtf8Raw, 0);
    }
};

uint32_t StringTableFRTest::stringBegins[maxStringBegins], StringTableFRTest::utf8RawSize;
uint8_t StringTableFRTest::utf8Raw[maxUtf8Raw];
uint16_t StringTableFRTest::stringCount, StringTableFRTest::stringBytes;
uint16_t StringTableFRTest::stringBeginsSize;

using CPUFLASHTranslationProviderFRTest = CPUFLASHTranslationProvider<StringTableFRTest>;

struct StringTableITTest {
    // this will get statically precomputed for each translation language separately
    static uint16_t stringCount, stringBytes;
    static uint16_t stringBeginsSize;
    static uint32_t utf8RawSize, stringBegins[maxStringBegins];
    // a piece of memory where the null-terminated strings are situated
    static uint8_t utf8Raw[maxUtf8Raw];

    static void Reset() {
        stringBeginsSize = 0;
        utf8RawSize = 0;
        fill(stringBegins, stringBegins + maxStringBegins, 0);
        fill(utf8Raw, utf8Raw + maxUtf8Raw, 0);
    }
};

uint32_t StringTableITTest::stringBegins[maxStringBegins], StringTableITTest::utf8RawSize;
uint8_t StringTableITTest::utf8Raw[maxUtf8Raw];
uint16_t StringTableITTest::stringCount, StringTableITTest::stringBytes;
uint16_t StringTableITTest::stringBeginsSize;

using CPUFLASHTranslationProviderITTest = CPUFLASHTranslationProvider<StringTableITTest>;

struct StringTablePLTest {
    // this will get statically precomputed for each translation language separately
    static uint16_t stringCount, stringBytes;
    static uint16_t stringBeginsSize;
    static uint32_t utf8RawSize, stringBegins[maxStringBegins];
    // a piece of memory where the null-terminated strings are situated
    static uint8_t utf8Raw[maxUtf8Raw];

    static void Reset() {
        stringBeginsSize = 0;
        utf8RawSize = 0;
        fill(stringBegins, stringBegins + maxStringBegins, 0);
        fill(utf8Raw, utf8Raw + maxUtf8Raw, 0);
    }
};

uint32_t StringTablePLTest::stringBegins[maxStringBegins], StringTablePLTest::utf8RawSize;
uint8_t StringTablePLTest::utf8Raw[maxUtf8Raw];
uint16_t StringTablePLTest::stringCount, StringTablePLTest::stringBytes;
uint16_t StringTablePLTest::stringBeginsSize;

using CPUFLASHTranslationProviderPLTest = CPUFLASHTranslationProvider<StringTablePLTest>;

struct StringTableJATest {
    // this will get statically precomputed for each translation language separately
    static uint16_t stringCount, stringBytes;
    static uint16_t stringBeginsSize;
    static uint32_t utf8RawSize, stringBegins[maxStringBegins];
    // a piece of memory where the null-terminated strings are situated
    static uint8_t utf8Raw[maxUtf8Raw];

    static void Reset() {
        stringBeginsSize = 0;
        utf8RawSize = 0;
        fill(stringBegins, stringBegins + maxStringBegins, 0);
        fill(utf8Raw, utf8Raw + maxUtf8Raw, 0);
    }
};

uint32_t StringTableJATest::stringBegins[maxStringBegins], StringTableJATest::utf8RawSize;
uint8_t StringTableJATest::utf8Raw[maxUtf8Raw];
uint16_t StringTableJATest::stringCount, StringTableJATest::stringBytes;
uint16_t StringTableJATest::stringBeginsSize;

using CPUFLASHTranslationProviderJATest = CPUFLASHTranslationProvider<StringTableJATest>;

struct StringTableUKTest {
    // this will get statically precomputed for each translation language separately
    static uint16_t stringCount, stringBytes;
    static uint16_t stringBeginsSize;
    static uint32_t utf8RawSize, stringBegins[maxStringBegins];
    // a piece of memory where the null-terminated strings are situated
    static uint8_t utf8Raw[maxUtf8Raw];

    static void Reset() {
        stringBeginsSize = 0;
        utf8RawSize = 0;
        fill(stringBegins, stringBegins + maxStringBegins, 0);
        fill(utf8Raw, utf8Raw + maxUtf8Raw, 0);
    }
};

uint32_t StringTableUKTest::stringBegins[maxStringBegins], StringTableUKTest::utf8RawSize;
uint8_t StringTableUKTest::utf8Raw[maxUtf8Raw];
uint16_t StringTableUKTest::stringCount, StringTableUKTest::stringBytes;
uint16_t StringTableUKTest::stringBeginsSize;

using CPUFLASHTranslationProviderUKTest = CPUFLASHTranslationProvider<StringTableUKTest>;

TEST_CASE("providerCPUFLASH::StringTableAt", "[translator]") {
    // simple test of several strings - setup first
    StringTableCSTest::Reset();

    StringTableCSTest::stringBegins[0] = 0;
    static const char str0[] = "first string";
    strcpy((char *)StringTableCSTest::utf8Raw, str0);

    StringTableCSTest::stringBegins[1] = sizeof(str0) + 1;
    static const char str1[] = "second long string";
    strcpy((char *)StringTableCSTest::utf8Raw + StringTableCSTest::stringBegins[1], str1);

    StringTableCSTest::stringBegins[2] = StringTableCSTest::stringBegins[1] + sizeof(str1) + 1;
    static const char str2[] = "příliš žluťoučký kůň";
    strcpy((char *)StringTableCSTest::utf8Raw + StringTableCSTest::stringBegins[2], str2);

    CPUFLASHTranslationProviderCSTest provider;

    // casting to const char * just to see the vars in the debugger like strings
    const char *s0 = (const char *)provider.StringTableAt(0);
    CHECK(!strcmp(s0, str0));

    const char *s1 = (const char *)provider.StringTableAt(1);
    CHECK(!strcmp(s1, str1));

    const char *s2 = (const char *)provider.StringTableAt(2);
    CHECK(!strcmp(s2, str2));
}

/// @returns number of bytes the strings require to store
pair<uint16_t, uint32_t> FillStringTable(const deque<string> &translatedStrings, uint32_t *stringBegins, uint8_t *utf8Raw) {
    uint8_t *utf8RawOrigin = utf8Raw;
    uint32_t *stringsBeginOrigin = stringBegins;
    for_each(translatedStrings.cbegin(), translatedStrings.cend(), [&](const string &s) {
        *stringBegins = utf8Raw - utf8RawOrigin;
        ++stringBegins;
        strcpy((char *)utf8Raw, s.c_str());
        utf8Raw += s.size();
        *utf8Raw = 0; // terminate the string
        ++utf8Raw;
    });
    return make_pair(stringBegins - stringsBeginOrigin, utf8Raw - utf8RawOrigin);
}

template <typename T>
void FillStringTable(const deque<string> &strings) {
    auto sizes = FillStringTable(strings, T::stringBegins, T::utf8Raw);
    T::stringBeginsSize = sizes.first;
    T::utf8RawSize = sizes.second;
}

/// Binary compare of providers' data - between the local one created in these unit tests (tstP)
/// and the one which was generated by the new python scripts and will be compiled into the firmware (compP)
template <typename T>
void CompareProviders(const T *tstP, const char *langCode) {
    Translations::Instance().ChangeLanguage(Translations::MakeLangCode(langCode));
    const CPUFLASHTranslationProviderBase *compP = dynamic_cast<const CPUFLASHTranslationProviderBase *>(Translations::Instance().CurrentProvider());
    REQUIRE(compP);
    const uint32_t *tstPSB = tstP->StringBegins();
    const uint8_t *tstPU8 = tstP->Utf8Raw();

    const uint32_t *compPSB = compP->StringBegins();
    const uint8_t *compPU8 = compP->Utf8Raw();

    // Now the tricky part - we need size of the arrays.
    // Since the arrays are expected to be of the same size and content,
    // we can abuse the testing one to provide the necessary size information
    REQUIRE(std::mismatch(tstPSB, tstPSB + T::RawData::stringBeginsSize, compPSB).first == tstPSB + T::RawData::stringBeginsSize);
    REQUIRE(std::mismatch(tstPU8, tstPU8 + T::RawData::utf8RawSize, compPU8).first == tstPU8 + T::RawData::utf8RawSize);
}

/// Binary compare of hash_tables' data
void CompareHashTables() {
    REQUIRE(std::mismatch(hash_table_ForComparison, hash_table_ForComparison + TPBSH::Buckets(),
                CPUFLASHTranslationProviderBase::hash_table.hash_table,
                [](const auto &a, const auto &b) { return std::tie(a.begin, a.end) == std::tie(b.begin, b.end); })
                .first
        == hash_table_ForComparison + TPBSH::Buckets());
    REQUIRE(std::mismatch(stringRecArrayForComparison, stringRecArrayForComparison + TPBSH::MaxStrings(),
                CPUFLASHTranslationProviderBase::hash_table.stringRecArray,
                [](const auto &a, const auto &b) { return std::tie(a.firstLetters, a.stringIndex) == std::tie(b.firstLetters, b.stringIndex); })
                .first
        == stringRecArrayForComparison + TPBSH::MaxStrings());
}

/// Ideally (not necessarily) run this test before the ComplexTest just to make sure the languages exist
TEST_CASE("providerCPUFLASH::Translations singleton", "[translator]") {
    REQUIRE(Translations::Instance().LangExists(Translations::MakeLangCode("cs")));
    REQUIRE(Translations::Instance().LangExists(Translations::MakeLangCode("de")));
    REQUIRE(Translations::Instance().LangExists(Translations::MakeLangCode("en")));
    REQUIRE(Translations::Instance().LangExists(Translations::MakeLangCode("es")));
    REQUIRE(Translations::Instance().LangExists(Translations::MakeLangCode("fr")));
    REQUIRE(Translations::Instance().LangExists(Translations::MakeLangCode("it")));
    REQUIRE(Translations::Instance().LangExists(Translations::MakeLangCode("pl")));
    REQUIRE(Translations::Instance().LangExists(Translations::MakeLangCode("ja")));
    REQUIRE(Translations::Instance().LangExists(Translations::MakeLangCode("uk")));
}

/// This is a complex test of the whole translation mechanism
/// We must prepare the search structures first and then lookup all the string keys
TEST_CASE("providerCPUFLASH::ComplexTest", "[translator]") {
    StringTableCSTest::Reset();

    // now we'll need more input files
    // cat Prusa-Firmware-Buddy_cs.po | grep msgstr | cut -b 8- | sed "s@\"@@g" > cs.txt
    // we already have the keys.txt, now we also need the translated texts
    // All that will be fed into the hash map (separate tests)
    // and the translated texts into the stringtable
    // After that, we may prove, that all the keys can be found correctly

    CPUFLASHTranslationProviderCSTest providerCS;
    CPUFLASHTranslationProviderDETest providerDE;
    CPUFLASHTranslationProviderESTest providerES;
    CPUFLASHTranslationProviderFRTest providerFR;
    CPUFLASHTranslationProviderITTest providerIT;
    CPUFLASHTranslationProviderPLTest providerPL;
    CPUFLASHTranslationProviderJATest providerJA;
    CPUFLASHTranslationProviderUKTest providerUK;
    deque<string> rawStringKeys;
    FillHashTableCPUFLASHProvider(CPUFLASHTranslationProviderBase::hash_table, "keys.txt", rawStringKeys);

    // now do a similar thing for the translated strings
    deque<string> csStrings, deStrings, esStrings, frStrings, itStrings, plStrings, jaStrings, ukStrings;
    REQUIRE(LoadTranslatedStringsFile("cs.txt", &csStrings));
    REQUIRE(LoadTranslatedStringsFile("de.txt", &deStrings));
    REQUIRE(LoadTranslatedStringsFile("es.txt", &esStrings));
    REQUIRE(LoadTranslatedStringsFile("fr.txt", &frStrings));
    REQUIRE(LoadTranslatedStringsFile("it.txt", &itStrings));
    REQUIRE(LoadTranslatedStringsFile("pl.txt", &plStrings));
    REQUIRE(LoadTranslatedStringsFile("ja.txt", &jaStrings));
    REQUIRE(LoadTranslatedStringsFile("uk.txt", &ukStrings));

    // need to have at least the same amount of translations like the keys (normally there will be an exact number of them)
    REQUIRE(rawStringKeys.size() <= csStrings.size());
    REQUIRE(rawStringKeys.size() <= deStrings.size());
    REQUIRE(rawStringKeys.size() <= esStrings.size());
    REQUIRE(rawStringKeys.size() <= frStrings.size());
    REQUIRE(rawStringKeys.size() <= itStrings.size());
    REQUIRE(rawStringKeys.size() <= plStrings.size());
    REQUIRE(rawStringKeys.size() <= jaStrings.size());
    REQUIRE(rawStringKeys.size() <= ukStrings.size());

    // now make the string table from cs.txt
    FillStringTable<StringTableCSTest>(csStrings);
    FillStringTable<StringTableDETest>(deStrings);
    FillStringTable<StringTableESTest>(esStrings);
    FillStringTable<StringTableFRTest>(frStrings);
    FillStringTable<StringTableITTest>(itStrings);
    FillStringTable<StringTablePLTest>(plStrings);
    FillStringTable<StringTableJATest>(jaStrings);
    FillStringTable<StringTableUKTest>(ukStrings);

    // prepare a map for comparison
    set<unichar> nonASCIICharacters;
    {
        // explicitly add characters from language names
        // Čeština, Español, Français, Japanese, Ukrainian
        static const uint8_t na[] = "ČšñçニホンゴУкраїнсьмов";
        string_view_utf8 nas = string_view_utf8::MakeRAM(na);
        StringReaderUtf8 reader(nas);
        unichar c;
        while ((c = reader.getUtf8Char()) != 0) {
            nonASCIICharacters.insert(c);
        }
    }
    REQUIRE(CheckAllTheStrings(rawStringKeys, csStrings, providerCS, nonASCIICharacters, "cs"));
    REQUIRE(CheckAllTheStrings(rawStringKeys, deStrings, providerDE, nonASCIICharacters, "de"));
    REQUIRE(CheckAllTheStrings(rawStringKeys, esStrings, providerES, nonASCIICharacters, "es"));
    REQUIRE(CheckAllTheStrings(rawStringKeys, frStrings, providerFR, nonASCIICharacters, "fr"));
    REQUIRE(CheckAllTheStrings(rawStringKeys, itStrings, providerIT, nonASCIICharacters, "it"));
    REQUIRE(CheckAllTheStrings(rawStringKeys, plStrings, providerPL, nonASCIICharacters, "pl"));
    REQUIRE(CheckAllTheStrings(rawStringKeys, jaStrings, providerJA, nonASCIICharacters, "ja"));
    REQUIRE(CheckAllTheStrings(rawStringKeys, ukStrings, providerUK, nonASCIICharacters, "uk"));

    CompareHashTables();

    CompareProviders(&providerCS, "cs");
    CompareProviders(&providerDE, "de");
    CompareProviders(&providerES, "es");
    CompareProviders(&providerFR, "fr");
    CompareProviders(&providerIT, "it");
    CompareProviders(&providerPL, "pl");
    CompareProviders(&providerJA, "ja");
    CompareProviders(&providerUK, "uk");

    // Check the content of generated non-ascii-chars - to see, if we have enough font bitmaps

    {
        for_each(nonASCIICharacters.begin(), nonASCIICharacters.end(), [](unichar c) {
            // with accents, we don't need the unaccent table anymore
            // but is important for character generation (newly added characters)
            // check, that we have this character in our temporary translation table
            CHECK_MESSAGE(NonASCIICharKnown(c), "Missing char ord=0x" << std::hex << c);
        });
    }
}
